"""
@File    :   sliding_puzzle.py
@Time    :   2024/04/02 14:45:17
@Author  :   Qrj
@Email   :   923207736@qq.com
@description   :   滑块验证码相关工具类
"""

import base64
import random
from io import BytesIO
from typing import Any

import ujson
from asyncer import asyncify
from PIL import Image, ImageFilter
from redis.asyncio import Redis

from src.utils.path import PathUtil
from src.utils.uuid import UuidUtil


class SlidingPuzzleCaptchaUtil:
    """滑块验证码工具类"""

    sliding_puzzle_captcha_hash_table_name = "sliding_puzzle_captcha_hash_table"
    SQUARE_SIZE = 36  # 正方形的边长
    CIRCLE_SIZE = 15  # 圆形的直径
    SHADOW_PEX = 130  # 阴影处理减弱的像素值
    # 随机获取半圆出现的方向
    SQUARE_UP_DOWN = ["up", "down"]
    SQUARE_LEFT_RIGHT = ["left", "right"]

    @staticmethod
    def random_get_sliding_image_path() -> str:
        """随机获取一个底图"""
        original_image_dir_path = PathUtil.get_static_sliding_puzzle_captcha_dir_path()
        # 遍历出所有底图
        original_images = PathUtil.rglob_pathlib(original_image_dir_path)
        original_images_list = [i for i in original_images]
        return random.choice(original_images_list).as_posix()

    @staticmethod
    def random_circle_location(x: int, y: int) -> Any:
        """根据正方形出现的位置(x,y)计算半圆型出现的位置"""
        up_down = random.choice(SlidingPuzzleCaptchaUtil.SQUARE_UP_DOWN)
        mx = x + random.randint(
            10,
            SlidingPuzzleCaptchaUtil.SQUARE_SIZE
            - SlidingPuzzleCaptchaUtil.CIRCLE_SIZE
            - 5,
        )
        if up_down == "up":
            my = y - SlidingPuzzleCaptchaUtil.CIRCLE_SIZE // 2
        else:
            my = (
                y
                + SlidingPuzzleCaptchaUtil.SQUARE_SIZE
                - SlidingPuzzleCaptchaUtil.CIRCLE_SIZE // 2
            )
        left_right = random.choice(SlidingPuzzleCaptchaUtil.SQUARE_LEFT_RIGHT)
        ny = y + random.randint(
            10,
            SlidingPuzzleCaptchaUtil.SQUARE_SIZE
            - SlidingPuzzleCaptchaUtil.CIRCLE_SIZE
            - 5,
        )
        if left_right == "left":
            nx = x - SlidingPuzzleCaptchaUtil.CIRCLE_SIZE // 2
        else:
            nx = (
                x
                + SlidingPuzzleCaptchaUtil.SQUARE_SIZE
                - SlidingPuzzleCaptchaUtil.CIRCLE_SIZE // 2
            )
        circle1_center = (
            mx + SlidingPuzzleCaptchaUtil.CIRCLE_SIZE // 2,
            my + SlidingPuzzleCaptchaUtil.CIRCLE_SIZE // 2,
        )
        circle2_center = (
            nx + SlidingPuzzleCaptchaUtil.CIRCLE_SIZE // 2,
            ny + SlidingPuzzleCaptchaUtil.CIRCLE_SIZE // 2,
        )
        return (mx, my, up_down, circle1_center), (nx, ny, left_right, circle2_center)

    @staticmethod
    def check_in_circle(circle, i, j, direct) -> bool:
        """检查当前坐标是否在半圆形区域内"""
        x, y, up_down, center = circle
        if direct == "out":
            if up_down == "left":
                x, y, h, z = (
                    x,
                    y,
                    x + SlidingPuzzleCaptchaUtil.CIRCLE_SIZE // 2,
                    y + SlidingPuzzleCaptchaUtil.CIRCLE_SIZE,
                )
            elif up_down == "up":
                x, y, h, z = (
                    x,
                    y,
                    x + SlidingPuzzleCaptchaUtil.CIRCLE_SIZE,
                    y + SlidingPuzzleCaptchaUtil.CIRCLE_SIZE // 2,
                )
            elif up_down == "right":
                x, y, h, z = (
                    x + SlidingPuzzleCaptchaUtil.CIRCLE_SIZE // 2,
                    y,
                    x + SlidingPuzzleCaptchaUtil.CIRCLE_SIZE,
                    y + SlidingPuzzleCaptchaUtil.CIRCLE_SIZE,
                )
            else:
                x, y, h, z = (
                    x,
                    y + SlidingPuzzleCaptchaUtil.CIRCLE_SIZE // 2,
                    x + SlidingPuzzleCaptchaUtil.CIRCLE_SIZE,
                    y + SlidingPuzzleCaptchaUtil.CIRCLE_SIZE,
                )
        else:
            if up_down == "left":
                x, y, h, z = (
                    x + SlidingPuzzleCaptchaUtil.CIRCLE_SIZE // 2,
                    y,
                    x + SlidingPuzzleCaptchaUtil.CIRCLE_SIZE,
                    y + SlidingPuzzleCaptchaUtil.CIRCLE_SIZE,
                )
            elif up_down == "up":
                x, y, h, z = (
                    x,
                    y + SlidingPuzzleCaptchaUtil.CIRCLE_SIZE // 2,
                    x + SlidingPuzzleCaptchaUtil.CIRCLE_SIZE,
                    y + SlidingPuzzleCaptchaUtil.CIRCLE_SIZE,
                )
            elif up_down == "right":
                x, y, h, z = (
                    x,
                    y,
                    x + SlidingPuzzleCaptchaUtil.CIRCLE_SIZE // 2,
                    y + SlidingPuzzleCaptchaUtil.CIRCLE_SIZE,
                )
            else:
                x, y, h, z = (
                    x,
                    y,
                    x + SlidingPuzzleCaptchaUtil.CIRCLE_SIZE,
                    y + SlidingPuzzleCaptchaUtil.CIRCLE_SIZE // 2,
                )
        # 上面的计算是为了进一步减少计算边长的范围
        if x <= i <= h and y <= j <= z:
            a, b = abs(center[0] - i), abs(center[1] - j)
            # 勾股定理
            if (a**2 + b**2) <= (SlidingPuzzleCaptchaUtil.CIRCLE_SIZE // 2) ** 2:
                return True
        return False

    @staticmethod
    def generate_captcha(
        sliding_image_path: str,
        sliding_captcha_width: int = 310,
        sliding_captcha_height: int = 155,
    ) -> tuple[tuple[int, int], str, str]:
        """生成滑块验证码"""
        # 打开图片
        img = Image.open(sliding_image_path)
        # 重置图片大小
        img = img.resize((sliding_captcha_width, sliding_captcha_height), Image.LANCZOS)
        # 获取随机的正方形区域
        width, height = img.size
        l = random.randint(
            width // 3,
            width
            - SlidingPuzzleCaptchaUtil.SQUARE_SIZE
            - SlidingPuzzleCaptchaUtil.CIRCLE_SIZE,
        )
        u = random.randint(
            height // 10,
            height
            - SlidingPuzzleCaptchaUtil.SQUARE_SIZE
            - SlidingPuzzleCaptchaUtil.CIRCLE_SIZE,
        )
        r, d = (
            l + SlidingPuzzleCaptchaUtil.SQUARE_SIZE,
            u + SlidingPuzzleCaptchaUtil.SQUARE_SIZE,
        )

        # 创建空白图
        bg = Image.new("L", (width, height))
        # 设置图片为RGBA模式
        if bg.mode != "RGBA":
            bg = bg.convert("RGBA")
        # 设置背景透明
        _, _, _, alpha = bg.split()
        alpha = alpha.point(lambda i: i > 0 and 1)
        bg.putalpha(alpha)

        # 确定两个圆形的坐标
        circles = SlidingPuzzleCaptchaUtil.random_circle_location(l, u)
        # 随机确定一个为内部圆(缺口)，一个为外部圆(凸出)
        n = random.randint(0, 1)
        for i in range(width):
            for j in range(height):
                # 判断是否在圆形内, n为外部圆 or 判断是否在正方形内
                if SlidingPuzzleCaptchaUtil.check_in_circle(
                    circles[n], i, j, "out"
                ) or (l <= i <= r and u <= j <= d):
                    # 内部半圆需要做排除
                    if SlidingPuzzleCaptchaUtil.check_in_circle(
                        circles[abs(1 - n)], i, j, "inner"
                    ):
                        continue
                    rgb = img.getpixel((i, j))
                    # 更新bg上的像素rgba值
                    bg.putpixel((i, j), rgb)
                    # 修改原图抠图位置的rgba值，阴影化
                    img.putpixel(
                        (i, j),
                        (
                            rgb[0] - SlidingPuzzleCaptchaUtil.SHADOW_PEX,
                            rgb[1] - SlidingPuzzleCaptchaUtil.SHADOW_PEX,
                            rgb[2] - SlidingPuzzleCaptchaUtil.SHADOW_PEX,
                            0,
                        ),
                    )

        # 扣出长条小图
        bg = bg.crop(
            (
                l - SlidingPuzzleCaptchaUtil.CIRCLE_SIZE // 2,
                0,
                l
                + SlidingPuzzleCaptchaUtil.SQUARE_SIZE
                + SlidingPuzzleCaptchaUtil.CIRCLE_SIZE // 2,
                sliding_captcha_height,
            )
        )

        # 模糊处理
        bg = bg.filter(ImageFilter.UnsharpMask)

        # 返回正方形的中心坐标，验证码验证判断的依据
        icon_center = (
            l + SlidingPuzzleCaptchaUtil.SQUARE_SIZE // 2,
            u + SlidingPuzzleCaptchaUtil.SQUARE_SIZE // 2,
        )

        # 返回二进制
        org_buf = BytesIO()
        bg_buf = BytesIO()
        img.save(org_buf, format="PNG")
        bg.save(bg_buf, format="PNG")
        org_buf.seek(0)
        bg_buf.seek(0)

        # 生成验证码bytes
        base_img_base64_str = (
            "data:image/png;base64," + base64.b64encode(org_buf.read()).decode()
        )
        slide_base64_str = (
            "data:image/png;base64," + base64.b64encode(bg_buf.read()).decode()
        )
        return icon_center, base_img_base64_str, slide_base64_str

    @staticmethod
    async def async_generate_captcha(
        sliding_image_path: str,
        sliding_captcha_width: int = 310,
        sliding_captcha_height: int = 155,
    ) -> tuple[tuple[int, int], str, str]:
        """异步生成滑块验证码"""
        return await asyncify(SlidingPuzzleCaptchaUtil.generate_captcha)(
            sliding_image_path=sliding_image_path,
            sliding_captcha_width=sliding_captcha_width,
            sliding_captcha_height=sliding_captcha_height,
        )

    @staticmethod
    async def create_sliding_captcha(
        data_redis_pool: Redis,
        code_id: str | None = None,
        sliding_image_path: str | None = None,
        sliding_captcha_width: int = 310,
        sliding_captcha_height: int = 155,
        batch_mapping: dict[str, str] | None = None,
    ) -> tuple[str, dict[str, int], str, str]:
        """向缓存中写入滑块验证码信息

        Args:
            data_redis_pool (Redis): 缓存连接池
            code_id (str | None, optional): 验证码ID
            sliding_image_path (str | None, optional): 滑块图路径
            sliding_captcha_width (int, optional): 滑块图宽度(px)
            sliding_captcha_height (int, optional): 滑块图高度(px)
            batch_mapping (dict[str, str] | None, optional): 需要批量插入的滑块验证码数据
        """
        # 获取底图
        if sliding_image_path is None:
            sliding_image_path = (
                SlidingPuzzleCaptchaUtil.random_get_sliding_image_path()
            )
        # 构建验证码唯一ID
        if code_id is None:
            code_id: str = UuidUtil.generate_uuid1()
        # 构建验证码答案与图像信息
        (
            code_answer,
            base_img_base64_str,
            slide_base64_str,
        ) = await SlidingPuzzleCaptchaUtil.async_generate_captcha(
            sliding_image_path,
            sliding_captcha_width=sliding_captcha_width,
            sliding_captcha_height=sliding_captcha_height,
        )
        code_answer = {"x": code_answer[0], "y": code_answer[1]}
        # 将图像验证码数据写入redis的hash表
        mapping = {
            code_id: ujson.dumps(
                {
                    "code_answer": code_answer,
                    "base_img_base64_str": base_img_base64_str,
                    "slide_base64_str": slide_base64_str,
                }
            )
        }
        # 如果有需要批量插入的图像验证码数据, 则一起插入
        if batch_mapping:
            mapping.update(batch_mapping)
        await data_redis_pool.hset(
            SlidingPuzzleCaptchaUtil.sliding_puzzle_captcha_hash_table_name,
            mapping=mapping,
        )
        return code_id, code_answer, base_img_base64_str, slide_base64_str

    @staticmethod
    async def random_get_sliding_captcha(
        data_redis_pool: Redis,
    ) -> tuple[str, str, str]:
        """随机获取滑块验证码ID与图像信息"""
        result = await data_redis_pool.hrandfield(
            SlidingPuzzleCaptchaUtil.sliding_puzzle_captcha_hash_table_name,
            count=1,
            withvalues=True,
        )
        if result:
            # 有返回信息则拆解出图像验证码ID与图像信息
            code_id, code_info = result
            code_info = ujson.loads(code_info)
            base_img_base64_str = code_info["base_img_base64_str"]
            slide_base64_str = code_info["slide_base64_str"]
            return code_id, base_img_base64_str, slide_base64_str
        else:
            # 没有返回信息则创建、插入并返回
            (
                code_id,
                _,
                base_img_base64_str,
                slide_base64_str,
            ) = await SlidingPuzzleCaptchaUtil.create_sliding_captcha(data_redis_pool)
            return code_id, base_img_base64_str, slide_base64_str

    @staticmethod
    async def validate_sliding_captcha(
        data_redis_pool: Redis,
        code_id: str,
        code_answer: dict[str, int],
        max_error_value: int = 3,
    ) -> bool | None:
        """单纯验证滑块验证码

        Args:
            data_redis_pool: redis连接池
            code_id (str): 图像验证码ID
            code_answer (dict[str, int]): 图像验证码答案
            error_value (int) : 误差值(px)

        Returns:
            bool | None: 是否验证成功, 未找到验证码信息则返回None
        """
        code_info = await data_redis_pool.hget(
            SlidingPuzzleCaptchaUtil.sliding_puzzle_captcha_hash_table_name, code_id
        )
        if code_info is None:
            # 未找到验证码信息则返回None
            return None
        # 拆解出验证码答案
        code_info = ujson.loads(code_info)
        cache_code_answer = code_info["code_answer"]
        # 验证并返回结果(误差在指定范围以内就算验证通过)
        result = abs(cache_code_answer["x"] - code_answer["x"]) <= max_error_value
        return result

    @staticmethod
    async def validate_and_del_sliding_captcha(
        data_redis_pool: Redis,
        code_id: str,
        code_answer: dict[str, int],
        max_error_value: int = 3,
    ) -> bool | None:
        """验证并删除滑块验证码

        Args:
            data_redis_pool: redis连接池
            code_id (str): 图像验证码ID
            code_answer (dict[str, int]): 图像验证码答案
            error_value (int) : 误差值(px)

        Returns:
            bool | None: 是否验证成功, 未找到验证码信息则返回None
        """
        # 验证图像验证码
        result = await SlidingPuzzleCaptchaUtil.validate_sliding_captcha(
            data_redis_pool, code_id, code_answer, max_error_value=max_error_value
        )
        # 找到验证码信息且验证结果正确时则将其从缓存中删除
        if result:
            await data_redis_pool.hdel(
                SlidingPuzzleCaptchaUtil.sliding_puzzle_captcha_hash_table_name, code_id
            )
        return result
